package data

import (
	"context"
	"errors"
	"fmt"
	"github.com/duke-git/lancet/convertor"
	"github.com/gogf/gf/util/gconv"
	jsoniter "github.com/json-iterator/go"
	"gorm.io/gorm"
	"gorm.io/gorm/clause"
	"time"

	"kratosEtcdWatcher/internal/biz"

	"github.com/go-kratos/kratos/v2/log"
)

const (
	lotteriesRedisKey = "lotteries"
	lotteriesRedisTTl = time.Minute * 10 // 本质上是为了防缓存击穿的，TTL可以设置的短一点

	lotteryRedisKeyFmt = "lottery:%v" // lottery:(activityId)
)

type lotteryRepo struct {
	data *Data
	log  *log.Helper
}

func NewLotteryRepo(data *Data, logger log.Logger) biz.LotteryRepo {
	return &lotteryRepo{
		data: data,
		log:  log.NewHelper(logger),
	}
}

// Notice 使用 rockscache，防缓存击穿！！！
func (r *lotteryRepo) GetLotteryByActivityId(ctx context.Context, actId uint32) (*biz.Lottery, error) {
	db := r.data.mysql.WithContext(ctx).Table(biz.LotteryTableName)

	lottery := &biz.Lottery{}
	redisKey := fmt.Sprintf(lotteryRedisKeyFmt, actId)

	val, errVal := r.data.rocksCache.Fetch2(ctx, redisKey, lotteriesRedisTTl, func() (string, error) {
		// 缓存过期 从数据库中获取 使用rockscache可以防止缓存击穿
		currLottery := &biz.Lottery{}
		errLst := db.Where("activity_id = ?", actId).Take(currLottery).Error
		// 防穿透
		if errLst == gorm.ErrRecordNotFound {
			return "", nil
		}

		return convertor.ToString(currLottery), nil
	})

	if errVal != nil {
		return nil, errVal
	}

	// Notice 数据库没数据，可以返回一个空的结构体，也可以报错 gorm.ErrRecordNotFound，看实际业务需求决定
	if val == "" {
		//return lottery, nil
		return nil, gorm.ErrRecordNotFound
	}

	errUnmarshal := jsoniter.UnmarshalFromString(val, &lottery)
	if errUnmarshal != nil {
		return nil, errors.New(fmt.Sprintf("GetLotteryById时Unmarshal错误了! err: %v", errUnmarshal))
	}

	return lottery, nil
}

func (r *lotteryRepo) GetAllLotteriesFromDBOrRedis(ctx context.Context) ([]biz.Lottery, error) {

	db := r.data.mysql.WithContext(ctx).Table(biz.LotteryTableName)

	// Notice 用rockscache：防止缓存击穿
	lotteries := make([]biz.Lottery, 0)
	val, errVal := r.data.rocksCache.Fetch2(ctx, lotteriesRedisKey, lotteriesRedisTTl, func() (string, error) {
		// 缓存过期 从数据库中获取 使用rockscache可以防止缓存击穿
		currLst := make([]biz.Lottery, 0)
		errLst := db.Find(&currLst).Error
		// 防穿透
		if errLst == gorm.ErrRecordNotFound {
			return "[]", nil
		}

		return convertor.ToString(currLst), nil
	})

	if errVal != nil {
		return nil, errVal
	}

	errUnmarshal := jsoniter.UnmarshalFromString(val, &lotteries)
	if errUnmarshal != nil {
		return nil, errors.New(fmt.Sprintf("GetAllLotteries时Unmarshal错误了! err: %v", errUnmarshal))
	}

	return lotteries, nil
}

// 删除本地lotteries缓存
func (r *lotteryRepo) RemoveLocalLotteriesCache(ctx context.Context) {

	// 1、删redis缓存
	errDelRedis := r.data.redis.Del(ctx, lotteriesRedisKey).Err()
	if errDelRedis != nil {
		// 删除失败 设置过期时间 3秒
		r.log.Errorf(fmt.Sprintf("removeLocalLotteriesCache时删除redis中的数据失败! err: %v", errDelRedis))
		r.data.redis.Expire(ctx, lotteriesRedisKey, time.Second*3)
	}

	// 2、将最新数据更新到etcd
	errEtcd := r.updateLotteriesEtcd(ctx)
	if errEtcd != nil {
		r.log.Errorf("errUpdateLotteriesEtcd: %v", errEtcd)
	}
}

// Notice 更新etcd的key，如果这个key对应的值有变化 etcdWatch到了之后会修改本地的缓存
func (r *lotteryRepo) updateLotteriesEtcd(ctx context.Context) error {

	// 获取最新的数据
	lotteries, errLotteries := r.GetAllLotteriesFromDBOrRedis(ctx)
	if errLotteries != nil {
		return errors.New(fmt.Sprintf("updateLotteriesEtcd时Get失败了! err: %v", errLotteries))
	}

	strRet, errMarshal := jsoniter.MarshalToString(lotteries)
	if errMarshal != nil {
		return errors.New(fmt.Sprintf("updateLotteriesEtcd时Marshal失败了! err: %v", errMarshal))
	}

	// Put
	_, errPut := r.data.etcdClient.Put(ctx, biz.LotteryKeyPrefix, strRet)
	if errPut != nil {
		return errors.New(fmt.Sprintf("updateLotteriesEtcd时Put失败了! err: %v", errPut))
	}

	return nil
}

func (r *lotteryRepo) UpsertLottery(ctx context.Context, g *biz.Lottery) error {

	err := r.data.mysql.WithContext(ctx).Table(biz.LotteryTableName).Clauses(clause.OnConflict{
		Columns:   []clause.Column{{Name: "activity_id"}},
		DoUpdates: clause.AssignmentColumns([]string{"total_limit", "daily_limit"}),
	}).Create(g).Error

	//  删redis缓存、删本地内存、修改etcd中的值
	// Notice 这是使用 atomic.Value 的方法
	r.RemoveLocalLotteriesCache(ctx)

	// Notice 这是使用 sync.Map 的方法
	//r.RemoveLocalLotteryCache(ctx, g.ActivityId)

	return err
}

func (r *lotteryRepo) RemoveLocalLotteryCache(ctx context.Context, actId uint32) {
	// 1、删redis缓存
	redisKey := fmt.Sprintf(lotteryRedisKeyFmt, actId)
	errDelRedis := r.data.redis.Del(ctx, redisKey).Err()
	if errDelRedis != nil {
		// 删除失败 设置过期时间 3秒
		r.log.Errorf(fmt.Sprintf("RemoveLocalLotteryCache时删除redis中的数据失败! actId: %v, err: %v", actId, errDelRedis))
		r.data.redis.Expire(ctx, redisKey, time.Second*3)
	}

	// 2、将最新数据更新到etcd
	errEtcd := r.updateLotteryEtcd(ctx, actId)
	if errEtcd != nil {
		r.log.Errorf("errEtcd: %v", errEtcd)
	}
}

func (r *lotteryRepo) updateLotteryEtcd(ctx context.Context, actId uint32) error {

	// 获取最新的数据
	lottery, errLottery := r.GetLotteryByActivityId(ctx, actId)
	if errLottery != nil {
		return errors.New(fmt.Sprintf("updateLotteryEtcd时Get失败了! err: %v", errLottery))
	}

	strRet, errMarshal := jsoniter.MarshalToString(lottery)
	if errMarshal != nil {
		return errors.New(fmt.Sprintf("updateLotteryEtcd时Marshal失败了! err: %v", errMarshal))
	}

	// Put
	cacheKey := biz.LotteryKeyPrefix + "/" + gconv.String(actId)
	_, errPut := r.data.etcdClient.Put(ctx, cacheKey, strRet)
	if errPut != nil {
		return errors.New(fmt.Sprintf("updateLotteryEtcd时Put失败了! err: %v", errPut))
	}

	return nil
}
